﻿using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace OxyPlot.CatExtension
{
    internal sealed class SimdDecimator
    {
        public const int MIN_DECIMATE_COUNT = 512;
        public const int MIN_DECIMATE_PARALLEL_COUNT = 1024 * 8;

        private static readonly int ProcessorCount = Environment.ProcessorCount;
        private readonly List<ScreenPoint>[] _buffers;

        private double offset_x;
        private double scale_x;

        private double offset_y;
        private double scale_y;

        public SimdDecimator()
        {            
            _buffers = new List<ScreenPoint>[ProcessorCount];
            for (int i = 0; i < ProcessorCount; i++)
            {
                _buffers[i] = [];
            }
        }

        public void Decimate(List<double> pointsX, List<double> pointsY, int start, int end, double offsetX, double scaleX, double offsetY, double scaleY, List<ScreenPoint> output)
        {
            if (pointsX == null || pointsX.Count == 0)
            {
                return;
            }

            offset_x = offsetX;
            scale_x = scaleX;
            offset_y = offsetY;
            scale_y = scaleY;

            var count = end - start + 1;

            if (count <= MIN_DECIMATE_COUNT)
            {
                for (int i = start; i <= end; i++)
                {
                    output.Add(Transform(pointsX[i], pointsY[i]));
                }
            }
            else if (count <= MIN_DECIMATE_PARALLEL_COUNT)
            {
                DecimateCore(pointsX, pointsY, start, end, output);
            }
            else
            {
                DecimateParallel(pointsX, pointsY, start, end, output);
            }

        }

        private void DecimateCore(List<double> pointsX, List<double> pointsY, int start, int end, List<ScreenPoint> output)
        {
            var buffer = output;
            var xSpan = CollectionsMarshal.AsSpan(pointsX).Slice(start, end - start + 1);
            var ySpan = CollectionsMarshal.AsSpan(pointsY).Slice(start, end - start + 1);

            start = 0;
            int currentX = (int)TransformX(xSpan[0]);

            var minY = ySpan[start];
            var maxY = minY;

            for (int i = 1; i < xSpan.Length; i++)
            {
                var x = TransformX(xSpan[i]);
                if (currentX != (int)x)
                {
                    var startY = ySpan[start];
                    if (minY != startY)
                    {
                        buffer.Add(new ScreenPoint(currentX, TransformY(minY)));
                        buffer.Add(new ScreenPoint(currentX, TransformY(startY)));
                        if (maxY != startY)
                        {
                            buffer.Add(new ScreenPoint(currentX, TransformY(maxY)));
                        }
                    }
                    else
                    {
                        buffer.Add(new ScreenPoint(currentX, TransformY(minY)));
                        if (maxY != minY)
                        {
                            buffer.Add(new ScreenPoint(currentX, TransformY(maxY)));
                        }
                    }

                    currentX = (int)x;
                    start = i;
                    minY = ySpan[start];
                    maxY = minY;
                }
                else
                {
                    minY = Math.Min(minY, ySpan[i]);
                    maxY = Math.Max(maxY, ySpan[i]);
                }
            }

            buffer.Add(Transform(xSpan[^1], ySpan[^1]));
        }

        private void DecimateParallel(List<double> pointsX, List<double> pointsY, int start, int end, List<ScreenPoint> output)
        {
            var len = (end - start + 1) / ProcessorCount;
            var lastLen = (end - start + 1) % ProcessorCount + len;
            Parallel.For(0, ProcessorCount, i =>
            {
                var buffer = _buffers[i];

                Span<double> xSpan;
                Span<double> ySpan;

                if (i == ProcessorCount - 1)
                {
                    xSpan = CollectionsMarshal.AsSpan(pointsX).Slice(start + i * len, lastLen);
                    ySpan = CollectionsMarshal.AsSpan(pointsY).Slice(start + i * len, lastLen);
                }
                else
                {
                    xSpan = CollectionsMarshal.AsSpan(pointsX).Slice(start + i * len, len);
                    ySpan = CollectionsMarshal.AsSpan(pointsY).Slice(start + i * len, len);
                }

                int currentX = (int)TransformX(xSpan[0]);

                int startIndex = 0;
                var minY = ySpan[startIndex];
                var maxY = minY;

                for (int j = 1; j < xSpan.Length; j++)
                {
                    var x = TransformX(xSpan[j]);
                    if (currentX != (int)x)
                    {
                        var startY = ySpan[startIndex];
                        if (minY != startY)
                        {
                            buffer.Add(new ScreenPoint(currentX, TransformY(minY)));
                            buffer.Add(new ScreenPoint(currentX, TransformY(startY)));
                            if (maxY != startY)
                            {
                                buffer.Add(new ScreenPoint(currentX, TransformY(maxY)));
                            }
                        }
                        else
                        {
                            buffer.Add(new ScreenPoint(currentX, TransformY(minY)));
                            if (maxY != minY)
                            {
                                buffer.Add(new ScreenPoint(currentX, TransformY(maxY)));
                            }
                        }

                        currentX = (int)x;
                        startIndex = j;
                        minY = ySpan[startIndex];
                        maxY = minY;
                    }
                    else
                    {
                        minY = Math.Min(minY, ySpan[j]);
                        maxY = Math.Max(maxY, ySpan[j]);
                    }
                }

                buffer.Add(Transform(xSpan[^1], ySpan[^1]));
            });

            for (int i = 0; i < _buffers.Length; i++)
            {
                output.AddRange(_buffers[i]);
                _buffers[i].Clear();
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private double TransformX(double x)
        {
            return (x - offset_x) * scale_x;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private double TransformY(double y)
        {
            return (y - offset_y) * scale_y;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private ScreenPoint Transform(double x, double y)
        {
            return new ScreenPoint((x - offset_x) * scale_x, (y - offset_y) * scale_y);
        }
    }
}
